Java 可重入锁 - ReentrantLock

前言

了解了Java的队列式同步器AQS的基本实现,接下来可以看看Java中频繁使用的可重入锁ReentrantLock。ReentrantLock是基于AQS实现的,内部类Sync继承了AQS,提供了公平锁和非公平锁。同Synchronized相比,ReentrantLock是代码实现的锁,而Synchronized是虚拟机上实现的锁,都是可重入的排他锁,即一个线程占有锁,其他线程必须等待锁的释放,同一个线程多次进入已将占有的锁,在效率方面,之前是ReentrantLock 效率更高,但后来Java对Synchronized进行了优化,效率目前来说差不多,ReentrantLock使用起来比Synchronized更加灵活,Synchronized使用来说更加方便,不用担心加锁释放锁的代码。

公平锁和非公平锁

先来看看公平锁和非公平锁的不同:

  • 公平锁:顾名思义,就是完全按照线程请求锁的先后顺序来获得锁,先到先得,如果等待锁的队列中还要其他线程,必须排在其后面等待
  • 非公平锁:在线程请求锁的同时,该锁被释放,则该线程会直接获得锁,无需经过等待队列,如果请求锁的同时锁被占有,则和公平锁一致,需要在等待队列中排队等候

公平锁和非公平锁各有各的应用场景,因为一个线程从唤醒到持有锁执行任务之间有着严重的延迟,假设一个线程A释放锁,需要唤醒其后继节点的线程B,B在从唤醒到持有锁有一段时间的延迟中,有一个线程C尝试获取锁,如果采用非公平锁,这时C会获得锁,如果C执行时间很短,在B唤醒前已经执行完毕,则B唤醒则直接获得锁执行,但如果C执行时间很长,在B唤醒前无法执行完毕,则B唤醒后获取锁失败会被重新挂起。

因此对于那些线程持有锁的时间较长的情况,采用公平锁更合适,减少不必要的唤醒(在非公平锁下唤醒得不到锁被重新挂起)带来的消耗,对于线程持有锁时间较短的情况,非公平锁可以提高效率,即不影响等待队列中线程运行,同时能运行完插队的线程。

总的来说,公平锁保证线程请求资源上的绝对公平,但是频繁的切换上下文,非公平锁可以降低上下文切换,降低性能开销,有更大的吞吐量,但是非公平锁有可能造成饥饿现象。

ReentrantLock

ReentrantLock是可重入独占锁,其内部是通过调用内部类Sync的方法实现对锁的控制,Sync是继承了AQS类,其使用了模板方法,子类只需实现tryAcquire和tryRelease方法返回获取锁和释放锁是否成功的boolean即可,如果获取锁线程会被挂起在同步队列中(在AQS中实现)因此ReentrantLock中Sync只需考虑判断是否获取锁成功以及判断是否释放锁,同时可以通过setExclusiveOwnerThread设置线程为独占锁的线程(传入null代表无线程占有锁,一般在释放锁时调用),AQS类的详细解读可以看 Java 同步器 - AQS。

非公平锁的实现

ReentrantLock考虑到系统的性能,默认是采用非公平锁,从其构造函数可以看出

1
2
3
4
5
6
7
8
// 默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

NonfairSync就是ReentrantLock中非公平锁的实现,其继承了Sync。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;

abstract void lock();

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果抢占锁成功,直接
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果是同一个线程,则可重入,state加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
...
}

static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

final void lock() {
// 当前没有线程持有锁,当前线程可直接尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

可以看到非公平锁lock()过程是,首先通过CAS设置State,如果设置成功,则说明获取锁成功,失败再调用acquire,调用tryAcquire方法,tryAcquire方法使用父类Sync的nonfairTryAcquire,这也是一次尝试通过CAS设置State,如果设置成功,则说明获取锁成功。可以看到如果有线程占有锁,会进行判断当前线程和占有锁的线程是否为同一个线程,如果是,state加1,获取锁成功,这里实现了可重入的功能。而state是判断能否释放锁的依据。

公平锁的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

final void lock() {
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 即使当前没有线程持有锁,如果等待队列中还有元素,也无法获得锁,需要等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果请求锁的线程是当前持有锁的线程,则返回true,实现可重入锁,state加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

其实公平锁和非公平锁的实现几乎类似,公平锁的lock方法直接调用acquire方法,没有非公平锁的抢占的操作,acquire最终会调用tryAcquire尝试获取锁(AQS中的实现),公平锁的tryAcquire方法在获取线程时比非公平锁多了一个hasQueuedPredecessors判断,判断等待队列中是否有前驱节点,如果有则不去尝试获取锁,直接判断获取失败。

释放锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// c==0代表重入已经全部释放
if (c == 0) {
free = true;
// 设置持有独占锁的信息为空
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

释放锁的过程是相同的,由于是可重入锁,state记录线程重入的次数,所以每当线程释放一次锁state减1,直到state的为0时代表线程重入锁已经全部释放,这时候可以释放锁给其他线程。由于tryRelease调用会减少state,所以是不允许未获得锁的线程释放锁的(即没有调用lock方法就调用release方法),否则会造成出错。

0%